Skip to content

Create Object API (Note/Article only for now) - #73

Open
2chanhaeng wants to merge 9 commits into
mainfrom
feat/obj-api
Open

Create Object API (Note/Article only for now)#73
2chanhaeng wants to merge 9 commits into
mainfrom
feat/obj-api

Conversation

@2chanhaeng

Copy link
Copy Markdown
Member

Resolve #9.

Adds an objects table (Note/Article; visibility public/unlisted/followers) with its migration.

GraphQL gains a createObject mutation for authenticated instance members, a Relay Object node, and an Actor.objects connection with totalCount.

Fedify side implements an object dispatcher at /users/{identifier}/{id} (Tombstone when deleted; followers-only objects not served) and a paginated outbox of synthetic Create activities (20 per page).

Side change: ignore mise.local.toml.

Verified with mise run check, mise run test (73 GraphQL, 3 models), and mise run dev startup.

AI Disclosure: Ideas presented by a human user were refined into a plan through Claude Code (claude-fable-5-1), and the draft plan was verified and partially modified by the user. It was implemented via Codex (gpt-6-astra), and after initial verification by Claude Code (claude-fable-5-1), it was manually reviewed, verified, and partially modified by a human.

Implement the [#9](#9) with Note and Article storage, authenticated createObject, Relay object queries, ActivityPub dispatch, and paginated synthetic Create activities in local actor outboxes.

AI provenance: The human user provided Fable with the scope and ideas for the implementation and had them draft a plan. The user read the draft, corrected any problematic parts, and had Astra handle the implementation. This session verified those changes and ran Claude Code with claude-fable-5 in a read-only review loop. After that, the human user read and verified.

Automated validation: mise run check; mise run test including the full build (73 GraphQL and 3 model tests); mise run dev startup and HTTP GraphQL Object introspection.

Assisted-by: Claude Code:claude-fable-5-1
Assisted-by: Codex:gpt-6-astra
Keep the pre-existing mise.local.toml ignore rule separate from the
ActivityPub object feature.

AI provenance: The user requested a Fable review loop. Codex reviewed and
committed this existing working-tree change separately after Fable noted
that it was unrelated to the feature. Codex did not author the rule.
No human manual verification was confirmed. Repository checks passed.

Assisted-by: Codex:gpt-6
- Rename `/users/<ACTOR_ID>/objects/<OBJECT_ID>` to `/users/<ACTOR_ID>/<OBJECT_ID>`
- Rename `ASObject` to `APObject`
Brand the objects.id and objects.actorId columns with the Uuid type in
@drfed/models so that Drizzle queries on the objects table require Uuid
values instead of plain strings.  Add validateUuid to @drfed/models/uuid
as a type guard and route all UUID generation and validation in
@drfed/graphql through that module instead of importing uuid directly.

Cast the actor identifier in the outbox dispatcher and the test fixture
values to Uuid, and declare the seed actor identifiers as const so they
satisfy the branded column types.  Also clarify the description of the
GraphQL Object.uuid field.

AI provenance: The human user authored and verified every code change
in this commit.  AI assistance was limited to analyzing the error
messages emitted by mise run check and to drafting this commit message.

Assisted-by: Claude Code:claude-fable-5-1
Comment thread .gitignore Outdated
@2chanhaeng
2chanhaeng requested a review from dodok8 September 12, 2026 13:02
Comment thread packages/graphql/src/object.ts
Comment thread packages/graphql/src/federation.ts Outdated
Actor.objects already filters out objects whose deleted timestamp is
set, but Query.node and Query.nodes still resolve deleted Actor and
Object rows by primary key, including their relations.  Add tests that
mark one row deleted and expect node to return null and nodes to return
null at that position, while a live sibling row keeps resolving.

The tests currently fail on purpose: they define the expected contract
for a follow-up change that adds a deleted IS NULL condition to the
Actor and Object drizzleNode loaders.  LocalActor is not covered
because the localActors table has no deleted column.

The tests were generated by Claude Code at the user's direction to
address the review comment below, and the user reviewed the result.

#73 (comment)

Assisted-by: Claude Code:claude-fable-5-1
The outbox previously emitted Create activities whose id pointed at
a URL that nothing served, so dereferencing it returned 404.  The
Create object dispatcher registered at /ap/creates/{id} in the
previous commit fixes that; this commit covers it with tests and
updates the outbox expectations to the new id and to the object
being referenced by IRI instead of embedded.

 -  Assert the Create URI layout from Context.getObjectUri().
 -  Dereference Create activities for public and unlisted objects and
    check their type, id, actor, object, and recipients.
 -  Reject followers-only, deleted, missing, malformed, remote-actor,
    deleted-actor, and wrong-host requests with 404, matching the
    object dispatcher.

Addresses the review comment at
#73 (comment)

The test changes were written by an AI assistant following the
reviewer's comment and the hackerspub implementation as a reference,
and were verified by the human author by running mise run test and
mise run check.

Assisted-by: Claude Code:claude-fable-5-1
@2chanhaeng
2chanhaeng requested a review from sij411 September 14, 2026 04:22
@dahlia dahlia added the enhancement New feature or request label Sep 14, 2026
@dahlia dahlia added this to the DrFed 0.1.0 milestone Sep 14, 2026
@dahlia dahlia moved this from Todo to In progress in NLnet NGI0 Commons Fund (2026) Sep 14, 2026

@dahlia dahlia left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The schema currently collapses ActivityPub addressing into a single visibility label, losing information that DrFed needs to inspect and compare implementations. Please preserve the original addressing and compute implementation-specific classifications for the dashboard.

The inline comments also cover timestamp precision in pagination, outbox counts, and soft-deletion behavior across relation queries. These need fixes before merging. The proposed table layout is a suggestion; preserving the protocol data is the requirement.

sensitive: boolean().notNull().default(false),
published: timestamp({ withTimezone: true })
.notNull()
.default(currentTimestamp),

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Converting the timestamp to Date can make Actor.objects skip rows. In PGlite, three objects with published = '2026-09-14T12:00:00.123456Z' produce one edge and hasNextPage: true for first: 1, then an empty page for the returned cursor. The Date conversion truncates the cursor timestamp to .123Z, so it no longer equals the stored value and the UUID tie-breaker cannot recover the remaining rows. The existing test uses millisecond-aligned dates and misses this case.

Could we preserve the database precision with Temporal.Instant? Drizzle ORM 1.0.0-beta.22 supports this through customType with fromDriver/toDriver; a PGlite round trip preserves the microseconds. Native Temporal support is tracked in drizzle-team/drizzle-orm#1776. The current composite cursor also preserves the ISO string, but its decoder returns a string, which the mapper needs to accept. The GraphQL DateTimeResolver rejects Temporal.Instant values and converts ISO strings back to Date, so it would need to change too. Please add a pagination regression test with microsecond timestamps. The full Temporal-to-GraphQL pagination path still needs verification.

)
.setCounter(
async (ctx, identifier) =>
(await findActiveActor(db, ctx, identifier))?.postsCount ?? null,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

postsCount counts objects that this outbox never returns. Creating a single FOLLOWERS object through the mutation yields totalItems: 1, but the outbox page has no items. Deleted objects are also excluded from the page query. The existing outbox test expects a count of 23 while returning only 21 items across its pages.

Please compute the outbox count with the same actor, deletion, and visibility filters as the page query. If we keep a cached count, it should count only objects matching those filters.

},
nodesQueryOptions: {
resolve: async (_, { ids }, __, ___, resolveNodes) =>
(await resolveNodes(ids)).map(filterDeleted),

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This filter hides deleted records only when they are the direct result of node or nodes. After marking an actor deleted, node(actorId) returns null, but node(objectId) { ... on Object { actor { uuid } } } still returns that actor. It also remains reachable through Instance.actors, and following its objects connection still returns content.

What should happen to an actor's objects when the actor is soft-deleted? The relation queries and their counts should follow that decision. The foreign key's ON DELETE CASCADE only handles physical deletion. Please add regression coverage for Object.actor, Instance.actors, and the deleted actor's objects connection alongside the direct node lookups.

type: objectTypeEnum().notNull(),
iri: text().notNull().unique(),
url: text(),
visibility: objectVisibilityEnum().notNull().default("public"),

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

visibility loses information that DrFed needs for debugging. Different combinations of to, cc, and audience can produce the same visibility label, so we cannot reconstruct the original addressing from this enum. The ActivityStreams audience-targeting model leaves much of its interpretation to implementations.

When Public is absent, Mastodon's initial classification checks for the author's followers collection in to, while Misskey checks both to and cc. A single stored enum cannot represent both interpretations.

Please preserve the addressing fields themselves. Visibility presets can still help users compose objects, but they should expand into explicit addressing before storage. The dashboard could show how Mastodon, Misskey, and other implementations would classify that addressing, with the implementation version and the reason for each result. I would compute those classifications in application code and expose the results through GraphQL, rather than store one implementation's interpretation as authoritative. The dashboard should label these as expected classifications, since actual access also depends on the receiving server's state and policies.

export type ObjectVisibility = (typeof objectVisibilityEnum.enumValues)[number];

/** ActivityPub objects authored by actors. */
export const objects = pgTable(

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we model addressing targets and collections explicitly? The many-to-many relation should allow an object to address individual actors, collections, or unresolved IRIs. Restricting the target to a collection would exclude direct actor addressing: ActivityStreams defines to as an Object or Link reference. The Public collection is also special: it cannot receive deliveries.

One possible layout is:

Table Purpose
resources Common references to actors, objects, and collections, including IRIs whose type is not yet known.
addressing Connect a source resource to a target resource, recording the property: to, cc, audience, and so on.
collections Collection type and metadata.
collection_items Membership, including position for ordered collections.

The table names and how the data is split between them are suggestions. The same target appearing in both to and cc must remain distinguishable. Keep audience distinct from to and cc. An activity's addressing and its embedded object's addressing should also be stored separately.

Addressing a followers collection is distinct from enumerating its members. Membership can change, so replacing the collection reference with its current members would lose the original intent. Observed membership and actual delivery recipients belong in separate records if we need that history. For received documents, retaining the original JSON-LD alongside these queryable relations would also preserve details that normalization may discard.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

Status: In progress

Development

Successfully merging this pull request may close these issues.

GraphQL API for creating objects

4 participants